/*
 * Copyright [2009] [Marcin Rzeźnicki]

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
 */
package mr.go.yaxc;

import java.lang.reflect.InvocationTargetException;
import java.util.ArrayDeque;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.Attributes;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.helpers.DefaultHandler;

final class XmlConfigurationHandler extends DefaultHandler implements
        ScopeHandler {

    private StringBuilder bodyText = new StringBuilder();
    private ArrayDeque<StringBuilder> bodyTexts =
            new ArrayDeque<StringBuilder>();
    private StringBuilder location = new StringBuilder("");
    private Locator locator;
    private final String namespace;
    private final Map<String, Object> singletons = new HashMap<String, Object>();
    private final ArrayDeque<ActionScope> stack = new ArrayDeque<ActionScope>();

    public XmlConfigurationHandler(
            String namespace) {
        this.namespace = namespace;
    }

    @Override
    public void addParam(Object o) {
        final List<Object> params = params();
        if (params != null) {
            params.add(o);
        }
    }

    @Override
    public void characters(char[] ch, int start, int length)
            throws SAXException {
        bodyText.append(ch,
                        start,
                        length);
    }

    @Override
    public void endDocument()
            throws SAXException {
        cleanup();
    }

    @Override
    public void endElement(String uri, String localName, String qName)
            throws SAXException {
        if (log.isDebugEnabled()) {
            log.debug("endElement(" + uri + "," + localName + "," + qName + ")");
            log.debug("  bodyText='" + bodyText + "'");
        }
        if (!stack.isEmpty()) {
            final ActionScope scope = stack.peek();
            final Action action = scope.action;
            if (scope.location.equals(location.toString())) {
                try {
                    if (log.isDebugEnabled()) {
                        log.debug("Popping action {}",
                                  action.toString());
                    }
                    stack.pop();
                    action.processed(bodyText.toString(),
                                     this);
                } catch (Exception ex) {
                    throw createSAXException("Configuration problem at " +
                            location.toString() + " after execution of " +
                            action.toString(),
                                             ex);
                }
            } else if (log.isDebugEnabled()) {
                log.debug(
                        "Leaving location {} with no action - next action {}",
                        location.toString(),
                        scope.location);
            }
        }
        bodyText = bodyTexts.pop();
        int slash = location.lastIndexOf("/");
        if (slash >= 0) {
            location.delete(slash,
                            location.length());
        } else {
            location = new StringBuilder();
        }

    }

    @Override
    public void fatalError(SAXParseException e)
            throws SAXException {
        cleanup();
        super.fatalError(e);
    }

    @Override
    public Logger getLogger() {
        return log;
    }

    @Override
    public String getNamespace() {
        return namespace;
    }

    @Override
    public Object getParentProperty(String property) {
        if (stack.isEmpty()) {
            return null;
        }
        final Map<String, Object> properties =
                stack.peek().action.getProperties();
        if (properties == null) {
            return null;
        }
        return properties.get(property);
    }

    @Override
    public Object getReference(String id) {
        final Object o = singletons.get(id);
        if (o == null) {
            log.warn("{} :{} is null",
                     location,
                     id);
        }
        return o;
    }

    @Override
    public List<Object> params() {
        if (stack.isEmpty()) {
            log.warn("{}: No params in scope",
                     location);
            return null;
        }
        final List<Object> params = stack.peek().action.getParams();
        if (params == null) {
            log.warn("{}: No params in scope",
                     location);
        }
        return params;
    }

    @Override
    public Object parentObject() {
        if (stack.isEmpty()) {
            log.warn("{} :Surrounding object not reachable",
                     location);
            return null;
        }
        final Object o = stack.peek().action.getObject();
        if (o == null) {
            log.warn("{} :Surrounding object not reachable",
                     location);
        }
        return o;
    }

    @Override
    public void setDocumentLocator(Locator locator) {
        this.locator = locator;
    }

    @Override
    public void setParentProperty(String property, Object value) {
        if (stack.isEmpty()) {
            log.warn("{}: setParentProperty - no parent",
                     location);
            throw new IllegalStateException("No parent");
        }
        final Map<String, Object> properties =
                stack.peek().action.getProperties();
        if (properties == null) {
            log.warn(
                    "{}: Parent did not define properties to be set",
                    location);
            throw new IllegalArgumentException("property");
        }
        properties.put(property,
                       value);
    }

    @Override
    public void setReference(String id, Object o) {
        if (singletons.containsKey(id)) {
            log.warn("{}: attempt to override {}",
                     location,
                     id);
            throw new IllegalArgumentException("id");
        }
        singletons.put(id,
                       o);
    }

    @Override
    public void startElement(
            String uri,
            String localName,
            String qName,
            Attributes attributes)
            throws SAXException {
        if (log.isDebugEnabled()) {
            log.debug("startElement(" + uri + "," + localName + "," + qName +
                    ")");
        }
        bodyTexts.push(bodyText);
        bodyText = new StringBuilder();
        final String name = (uri.isEmpty()) ? qName : localName;
        if (location.length() == 0) {
            location.append(name);
        } else {
            location.append('/').append(name);
        }
        final String loc = location.toString();
        if (!uri.equals(namespace)) {
            return;
        }
        Action action = Actions.create(loc);
        if (action != null) {
            final ActionScope scope = new ActionScope(action,
                                                      loc);
            try {
                if (log.isDebugEnabled()) {
                    log.debug(
                            "Installed action {} at {}",
                            action.toString(),
                            loc);
                }
                action.carryOut(name,
                                attributes,
                                this);
            } catch (Exception ex) {
                throw createSAXException("Configuration problem at " + loc +
                        " while executing " + action.toString(),
                                         ex);
            }
            stack.push(scope);
        }
    }

    @Override
    public void tryAddParam(Object o) {
        if (stack.isEmpty()) {
            return;
        }
        final List<Object> params = stack.peek().action.getParams();
        if (params != null) {
            params.add(o);
        }
    }

    private void cleanup() {
        singletons.clear();
        bodyText = new StringBuilder();
        location = new StringBuilder();
        stack.clear();
        bodyTexts.clear();
    }

    private SAXException createSAXException(String message, Exception e) {
        if ((e != null) && (e instanceof InvocationTargetException)) {
            Throwable t = ((InvocationTargetException) e).getTargetException();
            if ((t != null) && (t instanceof Exception)) {
                e = (Exception) t;
            }
        }
        if (locator != null) {
            String error =
                    "Error at line " + locator.getLineNumber() + " char " +
                    locator.getColumnNumber() + ": " + message;
            if (e != null) {
                return new SAXParseException(error,
                                             locator,
                                             e);
            } else {
                return new SAXParseException(error,
                                             locator);
            }
        }
        if (e != null) {
            return new SAXException(message,
                                    e);
        } else {
            return new SAXException(message);
        }

    }

    private static class ActionScope {

        private final Action action;
        private final String location;

        public ActionScope(
                Action action,
                String location) {
            this.action = action;
            this.location = location;
        }
    }
    private static final Logger log = LoggerFactory.getLogger(
            "mr.go.yaxc.XmlConfigurator");
}
